package hc.bre.rest.manage

import org.codehaus.groovy.control.CompilerConfiguration
import org.slf4j.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import hc.bre.ComponentRegistry
import hc.bre.cmn.BreException
import hc.bre.cmn.DiffResult
import hc.bre.cmn.DiffType
import hc.bre.cmn.RuleDefinition
import hc.bre.project.ProjectBuilder
import hc.bre.project.ProjectDefinition
import hc.bre.rest.admin.AuthUtil
import hc.bre.rest.admin.BreAdminConfig
import hc.bre.rest.admin.BreAdminService
import hc.bre.rest.admin.ObsoleteDataException
import hc.bre.rest.admin.ProjectEntry
import hc.bre.rest.admin.ScriptFileMode
import hc.bre.runner.ShellApplyRunner
import hc.bre.runner.ScriptEngineRunner
import java.lang.reflect.Method
import javax.annotation.PostConstruct

@Service
@groovy.transform.CompileStatic
public class MainService {
	protected static final Logger _log = LoggerFactory.getLogger(MainService.class);

	@Value('${rule.dir}')
	String ruledir
	
	@Autowired(required=false)
	BreAdminService adminService;
	
	@Autowired
	ScriptEngineRunner scriptEngineRunner;
	@PostConstruct
	void postConstruct() {
	}

	public ProjectDefinition createProject(Map argMap) {
		String rpath  = argMap['rpath'];

		if(adminService.config.projectList.find{ProjectEntry it -> it.rpath.toLowerCase() == rpath.toLowerCase() }) {
			throw new Exception("The project of ${rpath} has existed, please try another name.");
		}

		ProjectDefinition projDef = createDef(argMap) as ProjectDefinition

		String pcode = hc.Util.rpathPrjectCode(rpath);
		initRule(projDef,"variable","${pcode}/Variable")
		initRule(projDef,"data:setting","${pcode}/Setting")
		initRule(projDef,"function","${pcode}/Func")
		initRule(projDef,"data:policy:category","${pcode}/PolicyCategory")
		initRule(projDef,"data:policy:reason","${pcode}/PolicyReason")

		saveProjToRepo(projDef)
		saveRuleConfig(projDef)
		saveRuleGroovy(projDef)
		return projDef
	}

	private initRule(ProjectDefinition projDef, String rtype, String rpath) {
		projDef.list.add(rpath);

		def ruleDef = createDef([rtype:rtype,rpath:rpath]);

		saveRuleConfig(ruleDef)

		if(adminService.config.scriptFileMode == ScriptFileMode.justSelf)
			saveRuleGroovy(ruleDef)
	}

	ProjectDefinition loadProject(String rpath) {
		ProjectEntry ruleEty = getEntry(rpath);
		if(ruleEty == null) return null;

		ProjectDefinition projDef = (ProjectDefinition) loadRuleDef(ruleEty.rpath)

		projDef.ruleList = [];
		projDef.list.each{ String it ->
			try	//for more robust
			{
				projDef.ruleList.add(loadRuleDef(it));
			}
			catch(Throwable ex)
			{
				_log.error("failed to load rule of [{}], exception as below:{}",it.toString(),ex.toString())
			}
		}
		return projDef;
	}


	private RuleDefinition createDef(Map argMap) {
		String rtype  = argMap['rtype'];
		String rpath  = argMap['rpath'];

		Class clazz = hc.bre.ComponentRegistry.get(rtype).ruleClass;
		Method m = hc.Util.getStaticMethod(clazz, 'create');
		RuleDefinition ruleDef;
		ruleDef = (RuleDefinition) (m?m.invoke(clazz):clazz.getConstructor().newInstance());

		ruleDef.rpath = rpath;
		//use the default name when to call loadOrInit, no name from user.
		if(argMap.containsKey('rname'))
		{
			ruleDef.rname = argMap['rname'];
		}
		ruleDef.createdTime = hc.Util.dateTimeNow();
		ruleDef.createdUser = AuthUtil.userText();
		ruleDef.updatedTime = ruleDef.createdTime
		ruleDef.updatedUser = ruleDef.createdUser

		return ruleDef;
	}

	public Object deleteProject(String rpath) {
		ProjectDefinition ruleDef = loadRuleDef(rpath) as ProjectDefinition;

		ruleDef.list.each {
			deleteRuleFile(it)
		}

		deleteRuleFile(rpath);

		adminService.config.projectList.removeAll { it.rpath == rpath };
		adminService.save()

		return true;
	}

	public RuleDefinition createRuleDef(Map argMap) {
		String rpath  = argMap['rpath'];

		String projRpath = hc.Util.rpathPrjectCode(rpath) +'/Main'
		ProjectDefinition projDef = this.loadRuleDef(projRpath) as ProjectDefinition;
		//file or directory name is not case-sensitive in windows os;
		if(projDef.list.find {it.toLowerCase() == rpath.toLowerCase() }) {
			throw new Exception("The rule of ${rpath} has existed, please try another name.");
		}

		RuleDefinition ruleDef = createDef(argMap);

		saveRuleConfig(ruleDef);

		if(adminService.config.scriptFileMode == ScriptFileMode.justSelf)
			saveRuleGroovy(ruleDef);

		projDef.list.add(rpath);
		projDef.updatedTime = hc.Util.dateTimeNow();
		projDef.updatedUser = AuthUtil.userText();

		this.saveRuleConfig(projDef)
		saveRuleGroovy(projDef)

		return ruleDef;
	}

	public RuleDefinition loadRuleDef(String rpath) {
		String fileName ="${ruledir}/${rpath}.json"
		def rule = hc.Util.jsonRead(hc.Util.fileReadText(fileName));
		return RuleDefinition.loadRuleDef(rule as Map);
	}

	public RuleDefinition updateProject(Map ruleDefMap) {
		RuleDefinition ruleDef  = updateDef(ruleDefMap);

		//build groovy script
		saveRuleGroovy(ruleDef)
		
		//update project name in application config
		ProjectEntry projEntry = getEntry(ruleDef.rpath);
		if(ruleDef.rname != projEntry.rname)
		{
			projEntry.rname = ruleDef.rname;
			adminService.save();
		}

		return ruleDef;
	}

	public RuleDefinition updateRuleDef(Map ruleDefMap) {
		RuleDefinition ruleDef  = updateDef(ruleDefMap);

		//update project timestamp
		String projRpath = hc.Util.rpathPrjectCode(ruleDef.rpath) +'/Main'
		ProjectDefinition projDef = this.loadRuleDef(projRpath) as ProjectDefinition;
		projDef.updatedTime = hc.Util.dateTimeNow();
		projDef.updatedUser = AuthUtil.userText();

		this.saveRuleConfig(projDef)

		if(adminService.config.scriptFileMode == ScriptFileMode.justSelf)
		{
			saveRuleGroovy(ruleDef)
		}else {
			saveRuleGroovy(projDef)
		}

		return ruleDef;
	}

	private RuleDefinition updateDef(Map ruleDefMap) {
		RuleDefinition newDef = RuleDefinition.loadRuleDef(ruleDefMap)
		//check the script whether is valid, a exception is thrown if fail.
		newDef.buildRuleClass(ruledir);

		RuleDefinition oldDef = loadRuleDef(newDef.rpath);
		if(newDef.updatedTime  != oldDef.updatedTime)
			throw new ObsoleteDataException();

		newDef.updatedTime = hc.Util.dateTimeNow();
		newDef.updatedUser = AuthUtil.userText();

		saveRuleConfig(newDef)

		return newDef;
	}

	private ProjectEntry getEntry(String rpath)
	{
		adminService.config.projectList.find{it.rpath == rpath}
	}

	private void saveProjToRepo(RuleDefinition projDef)
	{
		adminService.config.projectList.add(new ProjectEntry().copy(projDef));
		adminService.save()
	}

	private void saveRuleConfig(RuleDefinition ruleDef)
	{
		hc.Util.fileWriteText(hc.Util.jsonWrite(ruleDef), ruledir + '/' + ruleDef.rpath +'.json');
	}

	private void saveRuleGroovy(RuleDefinition ruleDef)
	{
		String script = ruleDef.buildRuleScript();
		String fileName = ruledir + '/' + ruleDef.rpath +'.groovy';
		if(script)
		{
			hc.Util.fileWriteText(script, fileName);
		}
		else
		{
			hc.Util.fileDelete(fileName);
		};
	}

	public String singleScriptOfProject(String projPath)
	{
		if(adminService.config.scriptFileMode == ScriptFileMode.allInOne)
		{
			String fileName = ruledir + '/' + projPath +'.groovy';
			return hc.Util.fileReadText(fileName);
		}
		else
		{
			ProjectDefinition projDef = this.loadRuleDef(projPath) as ProjectDefinition;
			ProjectBuilder builder = new ProjectBuilder() {
				@Override
				public ScriptFileMode getScriptFileMode() {
					return ScriptFileMode.allInOne;
				}
			}
			
			builder.forRule(projDef);
			return builder.build();
		}
	}

	public Object runRuleDef(String rpath, Map data) {
		return scriptEngineRunner.run(rpath, data);
	
	}

	public Object deleteRuleDef(String rpath) {
		deleteRuleFile(rpath);

		//delete rule entry in project
		String projRpath = hc.Util.rpathPrjectCode(rpath) +'/Main'
		ProjectDefinition projDef = this.loadRuleDef(projRpath) as ProjectDefinition;
		projDef.list.removeAll { it == rpath };
		projDef.updatedTime = hc.Util.dateTimeNow();
		projDef.updatedUser = AuthUtil.userText();

		saveRuleConfig(projDef)
		if(adminService.config.scriptFileMode == ScriptFileMode.allInOne)
		{
			saveRuleGroovy(projDef)
		}
	}

	public Object deleteRuleFile(String rpath) {
		String fileName
		fileName ="${ruledir}/${rpath}.json";
		hc.Util.fileDelete(fileName);
		fileName ="${ruledir}/${rpath}.groovy";
		hc.Util.fileDelete(fileName);
	}

	public Object importProj(Map map)
	{
		if(getEntry(map.rpath as String))
		{
			throw new BreException("项目{rname=${map.rname},rpath=${map.rpath}}已存在，不能导入.");
		}

		//step 1 create the project rule
		RuleDefinition project = new ProjectDefinition().load(map);

		List<RuleDefinition> ruleList = project.ruleList;
		project.ruleList = [];
		saveProjToRepo(project);
		saveRuleConfig(project);

		//step 2 create all of rules in the project. care about the sequence because of reference relations between them.
		ruleList.each {
			saveRuleConfig(it);
		}

		if(adminService.config.scriptFileMode == ScriptFileMode.justSelf)
		{
			ruleList.each {saveRuleGroovy(it);}
		}

		//step 3 update project groovy script.
		saveRuleGroovy(project);

		return project;
	}


	public Object publishProj(Map otherProjDef)
	{
		ProjectDefinition other = new ProjectDefinition().load(otherProjDef);
		ProjectDefinition self = this.loadProject(other.rpath);

		if(self == null) return importProj(otherProjDef);

		LinkedHashMap<String, RuleDefinition> selfRuleMap = [:];
		self.ruleList?.each{
			selfRuleMap.put(it.rpath, it);
		}

		List <RuleDefinition> updatedList =[];
		other.ruleList?.each { otherItem ->
			def selfItem = selfRuleMap.get(otherItem.rpath)
			if(selfItem == null || selfItem.updatedTime < (otherItem.updatedTime as String))
			{
				saveRuleConfig(otherItem);
				updatedList.add(otherItem)
			}
			else {
				//identical or obsolete
			}

			selfRuleMap.remove(otherItem.rpath as String);
		}

		if(adminService.config.scriptFileMode == ScriptFileMode.justSelf)
		{
			updatedList.each {saveRuleGroovy(it); }
		}

		selfRuleMap.each {k,v->
			deleteRuleFile(k)
		}

		//update project last, self be replaced with other(except the obsolete).
		other.ruleList=[];
		saveRuleConfig(other);
		saveRuleGroovy(other);
		//TODO RETURN FRIENDLY RESULT
	};


	public DiffResult<?> compareProj(String selfProjRpath, Map otherProj)
	{
		ProjectDefinition self = this.loadProject(selfProjRpath);

		if(otherProj==null)
			return new DiffResult<ProjectDefinition>(type:DiffType.created,self:self,other:otherProj);

		ProjectDefinition other = new ProjectDefinition().load(otherProj);

		return self.compareTo(other);
	}

}
